/**
 * 
 */
package org.dragonfire.parser;

import java.util.Scanner;

import org.dragonfire.config.ResourceConfig;
import org.dragonfire.parser.exceptions.ParseException;
import org.dragonfire.parser.functions.Function;
import org.dragonfire.util.DMath;
import org.dragonfire.util.DString;

import com.stevesoft.pat.RegRes;
import com.stevesoft.pat.Regex;
import com.stevesoft.pat.ReplaceRule;
import com.stevesoft.pat.StringBufferLike;

/**
 * @author dandroid
 * @date Jul 11, 2006
 */
public class Equation {

	public static final ResourceConfig LANG = new ResourceConfig("org.dragonfire.parser.lang.parser");
	private boolean scientific = true;

	public boolean getScientific() {
		return scientific;
	}

	public void setScientific(boolean enabled) {
		scientific = enabled;
	}

	public void putFunction(String name, Function f) {
		// needs to be turned into a private object
		Function.putFunction(name, f);
	}

	/**
	 * The rule used to manage variables. By default, all variables equate to 0,
	 * unless a program defines a new variable rule using setVarRule()
	 */
	private ReplaceRule varRule = new ReplaceRule() {

		public void apply(StringBufferLike sb, RegRes rr) {
			sb.append("0"); // simply put a 0 in for each variable
		}
	};

	/**
	 * Changes the rule used to determine a variable's value.
	 * 
	 * @param rr
	 *            The new ReplaceRule
	 */
	public void setVarRule(ReplaceRule rr) {
		varRule = rr;
	}

	/**
	 * Gets the rule used to determine a variable's value.
	 * 
	 * @return The ReplaceRule used to define variable's values.
	 */
	public ReplaceRule getVarRule() {
		return varRule;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		System.out.println("Enter an expression:");
		String equ = in.nextLine();
		// Equation parser = new Equation(null);
		System.out.println(new Equation().eval(equ));
	}

	public static final String operRegex = "(?:[\\+\\*/\\|&><])\\1*";
	/**
	 * A regular expression that matches numbers produced by
	 * <code>Double.toString()</code>
	 */
	public static final String numberRegex = "\\b[-\\d\\.]+(?:[Ee][-\\d\\.])?\\b";
	/**
	 * A regular expression that will match a DASM label. (<code>name@module</code>)<br />
	 * Valid characters for the <code>name</code> and <code>module</code>
	 * are any letter
	 */
	public static final String labelRegex = "(?Q)\\b[A-z][A-z0-9_\\.]*(?:@[A-z][A-z0-9_\\.]*)?\\b";
	/**
	 * A regular expression that will match a quoted String
	 */
	public static final String quoteRegex = "\"(?:\\\\.|[^\"\\\\])*\"";
	// public static final String functionRegex =
	// "\\$([\\.\\$\\w\\d]*)\\b(?:{(?:([^\\${}]+)(?:,([^\\${}]+))*)?})?(?![{\\w\\d\\.\\$])";
	// public static final String functionRegex =
	// "\\$([\\$\\.\\w\\d]*)\\b(?:{(?:([^\\$,{}]+)(?:,([^\\$,{}]+))*)?})?(?![{\\w\\d\\.\\$])";
	// public static final String functionRegex =
	// "\\$([\\$\\.\\w\\d]*)\\b(?:{([^\\${}]*)})?(?![{\\w\\d\\.\\$])";
	public static final String functionRegex = "(?Q)\\$([\\w\\.\\$]*)((?@()))?";
	public static final String specificFunctionRegex = "(?Qi)\\$(NAME)((?@()))?";
	static {
		/*
		 * Create static references to various regex patterns used repeatedly.
		 * This defines custom patterns in the Regex class that allow shorthand
		 * access to the patterns like (??string) and (??variable)
		 */
		Regex.define("string", quoteRegex);
		Regex.define("variable", labelRegex);
		Regex.define("number", numberRegex);
		Regex.define("quote", quoteRegex);
		Regex.define("operator", operRegex);
	}

	/**
	 * Checks to make sure that all <code>(</code>, <code>{</code>,
	 * <code>[</code>, and <code>"</code> have a closing couterpart.
	 */
	public static void mismatchCheck(String equation) {
		int level = 0;
		int level2 = 0;
		int level3 = 0;
		boolean inQuotes = false;
		for (int i = 0; i < equation.length(); i++) {
			if (equation.charAt(i) == '"')
				inQuotes = !inQuotes;
			if (!inQuotes)
				switch (equation.charAt(i)) {
					case '(':
						level++;
						break;
					case ')':
						level--;
						break;
					case '{':
						level2++;
						break;
					case '}':
						level2--;
						break;
					case '[':
						level3++;
						break;
					case ']':
						level3--;
						break;
				}
		}
		if (level != 0)
			throw new ParseException("Parenthesis mismatch.");
		if (level2 != 0)
			throw new ParseException("Brace mismatch.");
		if (level3 != 0)
			throw new ParseException("Bracket mismatch.");
		if (inQuotes)
			throw new ParseException("Quotes mismatch.");
	}

	/**
	 * Strips the spaces out of a string that are not within double quotes.
	 * 
	 * @param equation
	 *            The equation to strip the spaces out of
	 * @return A string cleaned of spaces.
	 */
	public static String cleanSpaces(String equation) {
		Regex noSpace = new Regex("(?Q)\\s+");
		noSpace.setReplaceRule(""); // replace all spaces with nothing,
		// effectively deleting them
		equation = noSpace.replaceAll(equation); // execute!
		return equation;
	}

	/**
	 * This method evaluates functions in a string, replacing them with their
	 * return values.
	 * 
	 * @param equation
	 *            The equation to be parsed
	 * @return The equation passed to it with all functions evaluated.
	 */
	public String parseFunctions(String equation) {
		Regex funcParser = new Regex(functionRegex, new FunctionRule());
		return funcParser.replaceAll(equation);
	}

	public String parseFunction(String name, String equation) {
		Regex funcParser = new Regex(specificFunctionRegex.replaceFirst("NAME", name), new FunctionRule());
		return funcParser.replaceAll(equation);
	}

	public String eval(String equation) {
		mismatchCheck(equation);
		// remove all spaces that aren't in quotes to clean up the equation and
		// make it easier to parse
		equation = cleanSpaces(equation);
		// evaluate all literals (characters and numbers in non-deciaml bases)
		equation = evalLiterals(equation);
		// evaluate all the functions. Functions must be evaluated prior to
		// parenthesis because
		// arguments are contained within parenthesis and the parser would choke
		// and die on the commas
		// when it tries to parse them as a simple expression.
		equation = parseFunctions(equation);
		// recursively evaluate parenthesis subexpressions
		Regex paren = new Regex("(?Q)\\([^\\(\\)]*\\)", new ParenRule());
		while (paren.search(equation))
			// don't stop until all ()'s have been parsed
			equation = paren.replaceAll(equation);
		// all that's left are variables, parse them!
		equation = parseVariables(equation);
		// if (!Pattern.matches(numberRegex + "(" + operRegex + numberRegex +
		// ")*", equation)) {
		// throw new RuntimeException("Invalid Expression Format: " + equation);
		// }
		equation = equation.replaceAll("-", "+-");
		if (equation.length() == 0)
			return "";
		if (equation.charAt(0) == '+')
			equation = "0" + equation;
		// MARKER Order of operations
		equation = cleanSpaces(equation);
		if (scientific) {
			String[] ops = new String[] {
					"[%\\*/]", "[\\+]", "[&\\|\\^]", "<<|>>", "<|>|===|==|!=|>=|<="
			};
			for (String s : ops)
				equation = evalOp(equation, s);
		}
		equation = evalOp(equation, "(??operator)");
		while (Regex.matches("\\+\\-", equation)) {
			equation = Regex.replaceAll(equation, "\\+\\-", "-");
		}
		return equation;
	}

	public String parseVariables(String equation) {
		Regex equate = new Regex(labelRegex, varRule);
		while (equate.search(equation))
			equation = eval(equate.replaceAll(equation));
		return equation;
	}

	/**
	 * Parses an equation and converts non-numerical constants into their
	 * numerical value.<br />
	 * Constants include:
	 * <ul>
	 * <li>Characters - <b>Any</b> character between two single quotes, such
	 * as 'A' or '$'</li>
	 * <li>Base-Prefix Numbers - A number that uses a prefix character to
	 * signify a base other than 10. The number portion must contain at least
	 * two digits ($5 will not be picked up, it is necessary to use $05). <br />
	 * Supported prefixes:
	 * <ul>
	 * <li><code>$</code> - Base 16 (Hexadecimal)</li>
	 * <li><code>@</code> - Base 8 (Octal)</li>
	 * <li><code>%</code> - Base 2 (Binary)</li>
	 * </ul>
	 * </li>
	 * <li>Base-Post Numbers - A number that uses a postfix character to
	 * signify a base other than 10. All postfixes are treated as <code>d</code>
	 * (decimal). Postfixes are not case sensitive and the number portion must
	 * contain at least two digits (5h will not be picked up, it is necessary to
	 * use 05h). <br />
	 * Supported postfixes:
	 * <ul>
	 * <li><code>h</code> - Base 16 (Hexadecimal)</li>
	 * <li><code>d</code> - Base 10 (decimal, default base)</li>
	 * <li><code>o</code> - Base 8 (Octal)</li>
	 * <li><code>b</code> - Base 2 (Binary)</li>
	 * </ul>
	 * </li>
	 * </ul>
	 * @param equ
	 *            The equation to parse
	 * @return An equation with all non-number literal values converted into
	 *         their numerical equvilants.
	 */
	public static String evalLiterals(String equ) {
		equ = equ.replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
		Regex charrule = new Regex("\\'.\\'");
		charrule.setReplaceRule(new CharRule());
		equ = charrule.replaceAll(equ);
		Regex basebrule = new Regex("(?iQ)\\b[%@\\$][\\dabcdef]{2,}\\b");
		basebrule.setReplaceRule(new BaseBeginRule());
		equ = basebrule.replaceAll(equ);
		Regex baseerule = new Regex("(?iQ)\\b[\\dabcdef]{2,}[hobd]\\b");
		baseerule.setReplaceRule(new BaseEndRule());
		equ = baseerule.replaceAll(equ);
		// equ = equ.replaceAll("(?=>.)-", "+-");
		return equ;
	}

	public boolean evalBoolean(String equation) {
		if (equation == null || equation.equals(""))
			return false;
		equation = eval(equation);
		return !equation.equals("0.0") && !equation.equals("0") && !equation.equals("\"\"") && !equation.equals("");
	}

	public String evalString(String equation) {
		String equ = eval(equation);
		try {
			return DString.extractString(equ);
		} catch (Exception e) {
			throw new ParseException("ERROR_STRING_REQUIRED", equation);
		}
	}

	public int evalInt(String equation) {
		String equ = eval(equation);
		try {
			return (int) DMath.parseDouble(equ);
		} catch (NumberFormatException ne) {
			throw new ParseException("ERROR_INT_REQUIRED", equation);
		}
	}

	public double evalDouble(String equation) {
		String equ = eval(equation);
		try {
			return DMath.parseDouble(equ);
		} catch (NumberFormatException ne) {
			throw new ParseException("ERROR_NUM_REQUIRED", equation);
		}
	}

	private static String evalOp(String equation, String operators) {
		if (isString(equation))
			return equation;
		Regex eval = new Regex("(?:((??quote)|(??number))(" + operators + ")((??quote)|(??number)))|(??quote)");
		eval.setReplaceRule(new EvalRule());
		// while (eval.search(equation) && !isString(equation)) {
		equation = eval.replaceAll(eval.replaceAll(eval.replaceAll(equation)));
		// }
		return equation;
	}

	private static class BaseBeginRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			int base = 10;
			String match = rr.stringMatched();
			switch (match.charAt(0)) {
				case '%':
					base = 2;
					break;
				case '$':
					base = 16;
					break;
				case '@':
					base = 8;
					break;
				default:
			}
			sb.append(Integer.parseInt(match.substring(1), base) + "");
		}
	}

	private static class BaseEndRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			int base = 10;
			String match = rr.stringMatched();
			switch (match.toLowerCase().charAt(match.length() - 1)) {
				case 'b':
					base = 2;
					break;
				case 'h':
					base = 16;
					break;
				case 'o':
					base = 8;
					break;
				default:
			}
			sb.append(Integer.parseInt(match.substring(0, match.length() - 1), base) + "");
		}
	}

	private static class CharRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			char value = rr.stringMatched().substring(1, rr.stringMatched().length() - 1).charAt(0);
			sb.append(((int) value) + "");
		}
	}

	private class ParenRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			// TODO Make this obey sci or not-sci
			String value = eval(rr.stringMatched().substring(1, rr.stringMatched().length() - 1));
			sb.append(value);
		}
	}

	private class FunctionRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			String name = rr.stringMatched(1);
			String[] params = rr.stringMatched(2) != null ? DString.splitTopLevel(rr.stringMatched(2).substring(1,
					rr.stringMatched(2).length() - 1), ',') : new String[0];
			for (int i = 0; i < params.length; i++)
				params[i] = eval(params[i]);
			sb.append(eval(Function.parseFunction(Equation.this, name, params)));
		}
	}

	private static class EvalRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			if (rr.stringMatched(1) == null) {
				sb.append(rr.stringMatched());
				return;
			}
			double num1 = 0, num2 = 0;
			String str1 = "", str2 = "";
			String operand1 = rr.stringMatched(1);
			String operand2 = rr.stringMatched(3);
			int mode = 0;
			if (!isString(operand1))
				num1 = DMath.parseDouble(operand1);
			else {
				str1 = DString.extractString(operand1);
				mode |= 1;
			}
			if (!isString(operand2))
				num2 = DMath.parseDouble(operand2);
			else {
				str2 = DString.extractString(operand2);
				mode |= 2;
			}
			String op = rr.stringMatched(2);
			String output = "";
			if (op.equals("+"))
				switch (mode) {
					case 0:
						output += num1 + num2;
						break;
					case 1:
						output = "\"" + str1 + num2 + "\"";
						break;
					case 2:
						output = "\"" + num1 + str2 + "\"";
						break;
					case 3:
						output = "\"" + str1 + str2 + "\"";
				}
			else if (op.equals("-") && mode == 0)
				output += num1 - num2;
			else if (op.equals("*") && mode == 0)
				output += num1 * num2;
			else if (op.equals("/") && mode == 0)
				output += num1 / num2;
			else if (op.equals("%") && mode == 0)
				output += num1 % num2;
			else if (op.equals("&") && mode == 0)
				output += (int) num1 & (int) num2;
			else if (op.equals("|") && mode == 0)
				output += (int) num1 | (int) num2;
			else if (op.equals("^") && mode == 0)
				output += (int) num1 ^ (int) num2;
			else if (op.equals("<"))
				switch (mode) {
					case 0:
						output += num1 < num2 ? "1" : "0";
						break;
					case 3:
						output = str1.compareTo(str2) < 0 ? "1" : "0";
				}
			else if (op.equals("<="))
				switch (mode) {
					case 0:
						output += num1 <= num2 ? "1" : "0";
						break;
					case 3:
						output = str1.compareTo(str2) <= 0 ? "1" : "0";
				}
			else if (op.equals(">"))
				switch (mode) {
					case 0:
						output += num1 > num2 ? "1" : "0";
						break;
					case 3:
						output = str1.compareTo(str2) > 0 ? "1" : "0";
				}
			else if (op.equals(">="))
				switch (mode) {
					case 0:
						output += num1 >= num2 ? "1" : "0";
						break;
					case 3:
						output = str1.compareTo(str2) >= 0 ? "1" : "0";
				}
			else if (op.equals("=="))
				switch (mode) {
					case 0:
						output += num1 == num2 ? "1" : "0";
						break;
					case 1:
						output += DMath.parseDouble(str1) == num2 ? "1" : "0";
						break;
					case 2:
						output += DMath.parseDouble(str2) == num1 ? "1" : "0";
						break;
					case 3:
						output = str1.equalsIgnoreCase(str2) ? "1" : "0";
						break;
				}
			else if (op.equals("==="))
				switch (mode) {
					case 0:
						output += num1 == num2 ? "1" : "0";
						break;
					case 3:
						output = str1.equals(str2) ? "1" : "0";
						break;
					default:
						output = "0";
				}
			else if (op.equals(">>"))
				switch (mode) {
					case 0:
						output += (int) num1 >> (int) num2;
						break;
					case 1:
						output = "\"" + DString.cycleRight(str1, (int) num2) + "\"";
				}
			else if (op.equals("<<"))
				switch (mode) {
					case 0:
						output += (int) num1 << (int) num2;
						break;
					case 1:
						output = "\"" + DString.cycleLeft(str1, (int) num2) + "\"";
				}
			else if (op.equals("!="))
				switch (mode) {
					case 0:
						output += num1 != num2 ? "1" : "0";
						break;
					case 3:
						output = !str1.equals(str2) ? "1" : "0";
				}
			else {
				throw new ParseException("ERROR_UNKNOWN_OPERATOR", op);
			}
			if (output.equals("")) {
				String error = "The operator " + op + " is not applicable for the arguments (";
				error += (mode & 1) == 0 ? "number," : "string,";
				error += (mode & 2) == 0 ? "number)." : "string).";
				throw new RuntimeException(error);
			}
			/*
			 * if (mode > 0) output = "\"" + output + "\"";
			 */
			sb.append(output);
		}
	}

	public static boolean isString(String string) {
		return (new Regex("^(??quote)$")).search(string);
	}

	public static boolean isVariable(String name) {
		return (new Regex("^(??variable)$")).search(name);
	}

	/**
	 * Determins if a String is a valid double representation
	 * 
	 * @param number
	 *            The String to check
	 * @return True if the String is a valid double<br />
	 *         False if it isn't
	 */
	public static boolean isDouble(String number) {
		return (new Regex("^(??number)$")).search(number);
	}
}
